package handler

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"

	"database/sql"

	"gitee.com/hp-huiw/family-expense-api/internal/dto"
	"gitee.com/hp-huiw/family-expense-api/internal/service"
	"gitee.com/hp-huiw/my-go-core/date"
	"gitee.com/hp-huiw/my-go-core/errorcode"

	libsecurity "gitee.com/hp-huiw/my-go-core/security"
	"gitee.com/hp-huiw/my-go-core/servercode"
	"gitee.com/hp-huiw/my-go-core/util"
)

type AuthenticationHandler interface {
	CheckOnUsername(ctx *gin.Context)
	Register(ctx *gin.Context)
	Unregister(ctx *gin.Context)
	Login(ctx *gin.Context)
	RefreshToken(ctx *gin.Context)
	SwitchAccountGroup(ctx *gin.Context)
	Logout(ctx *gin.Context)
}

// Returns the instance of private member, userHandler, which implements UserHandler interface.
func NewAuthenticationHandler(db *sql.DB) AuthenticationHandler {
	return &authenticationHandler{
		accountGroupService:   service.NewAccountGroupService(db),
		authenticationService: service.NewAuthenticationService(db),
		userService:           service.NewUserService(db),
	}
}

// Matches /check-on-username of http get action.
func (h *authenticationHandler) CheckOnUsername(ctx *gin.Context) {
	c := ctx.Request.Context()
	username := ctx.Query("username")
	result, err := h.userService.CheckOnUsername(c, username)

	if err != nil {
		ctx.Error(err)
		return
	}

	SendDataResponse(ctx, map[string]bool{
		"usernameExists": result,
	})
}

// Matches /register of http post action.
func (h *authenticationHandler) Register(ctx *gin.Context) {
	var postedData struct {
		Username string `json:"username" binding:"required"`
		Password string `json:"password" binding:"required"`
		FullName string `json:"fullName" binding:"required"`
	}

	// Binds data from posted json.
	if err := ctx.ShouldBindJSON(&postedData); err != nil {
		ctx.Error(err)
		return
	}

	c := ctx.Request.Context()

	err := h.accountGroupService.Register(c, postedData.Username, postedData.Password, postedData.FullName)

	if err != nil {
		ctx.Error(err)
		return
	}

	SendStatusResponse(ctx, http.StatusNoContent)
}

// Matches /unregister of http post action.
func (h *authenticationHandler) Unregister(ctx *gin.Context) {
	var postedData struct {
		Password string `json:"password" binding:"required"`
	}

	// Binds data from posted json.
	if err := ctx.ShouldBindJSON(&postedData); err != nil {
		ctx.Error(err)
		return
	}

	c := ctx.Request.Context()
	password := postedData.Password
	claims := GetClaims(ctx)
	userID := claims.UserID
	username := claims.Username

	validatePassword, err := h.userService.ValidatePassword(c, username, password)

	if err != nil {
		ctx.Error(err)
		return
	}

	if validatePassword {
		if err := h.accountGroupService.Unregister(c, userID, username); err != nil {
			h.abortWithStatusJSON(ctx, errorcode.ErrorServiceUnexpectedError)
		} else {
			SendDataResponse(ctx, map[string]string{"error": ""})
		}
	} else {
		SendDataResponse(ctx, map[string]string{"error": errorcode.ErrorUserAccountBadCredential})
	}
}

// Matches /login of http post action.
func (h *authenticationHandler) Login(ctx *gin.Context) {
	// If the posted data structure is not shared by different handler functions you can define it here.
	var postedData struct {
		Username   string                     `json:"username" binding:"required"`
		Password   string                     `json:"password" binding:"required"`
		DeviceInfo *dto.UserDeviceInfoRequest `json:"deviceInfo"`
		SystemInfo *dto.UserSystemInfoRequest `json:"systemInfo"`
	}

	if err := ctx.ShouldBindJSON(&postedData); err != nil {
		ctx.Error(err)
		return
	}

	c := ctx.Request.Context()

	myUserDetails, errorCode := h.authenticationService.AuthenticateByCredential(c,
		postedData.Username,
		postedData.Password,
		ctx.ClientIP(),
		postedData.DeviceInfo,
		postedData.SystemInfo)

	// All response data format is for the client which works with spring security rest.
	if errorCode == "" {
		// Responses with specified http status code and json format result.
		SendDataResponse(ctx, myUserDetails)
	} else {
		h.abortWithStatusJSON(ctx, errorCode)
	}
}

// Matches /refresh-token of http post action.
func (h *authenticationHandler) RefreshToken(ctx *gin.Context) {
	// If the posted data structure is not shared by different handler functions you can define it here.
	var postedData struct {
		RefreshToken string                     `json:"refreshToken" binding:"required"`
		DeviceInfo   *dto.UserDeviceInfoRequest `json:"deviceInfo"`
		SystemInfo   *dto.UserSystemInfoRequest `json:"systemInfo"`
	}

	// Binds data from posted json.
	if err := ctx.ShouldBindJSON(&postedData); err != nil {
		ctx.Error(err)
		return
	}

	c := ctx.Request.Context()

	myUserDetails, errorCode := h.authenticationService.RefreshToken(c, postedData.RefreshToken, ctx.ClientIP(), postedData.DeviceInfo, postedData.SystemInfo)

	// All response data format is for the client which works with spring security rest.
	if errorCode == "" {
		// Responses with specified http status code and json format result.
		SendDataResponse(ctx, myUserDetails)
	} else {
		// This response data format is for the client which works with spring security rest.
		h.abortWithStatusJSON(ctx, errorCode)
	}
}

// Matches /switch-account-group of http post action.
func (h *authenticationHandler) SwitchAccountGroup(ctx *gin.Context) {
	// If the posted data structure is not shared by different handler functions you can define it here.
	var postedData struct {
		AccountGroupID string                     `json:"accountGroupID" binding:"required"`
		DeviceInfo     *dto.UserDeviceInfoRequest `json:"deviceInfo"`
		SystemInfo     *dto.UserSystemInfoRequest `json:"systemInfo"`
	}

	// Binds data from posted json.
	if err := ctx.ShouldBindJSON(&postedData); err != nil {
		ctx.Error(err)
		return
	}

	c := ctx.Request.Context()
	claims := GetClaims(ctx)
	userID := claims.UserID
	username := claims.Username

	myUserDetails, errorCode := h.authenticationService.SwitchAccountGroup(c, postedData.AccountGroupID, userID, username, ctx.ClientIP(), postedData.DeviceInfo, postedData.SystemInfo)

	// All response data format is for the client which works with spring security rest.
	if errorCode == "" {
		// Responses with specified http status code and json format result.
		SendDataResponse(ctx, myUserDetails)
	} else {
		h.abortWithStatusJSON(ctx, errorCode)
	}
}

// Matches /logout of http post action.
func (h *authenticationHandler) Logout(ctx *gin.Context) {
	c := ctx.Request.Context()

	// Gets authorization header info.
	authorizationHeader := strings.TrimSpace(ctx.GetHeader(util.HttpHeaderAuthorization))

	// Gets token from header.
	_, _, token := libsecurity.CheckAuthorizationHeader(authorizationHeader)

	// Logs out by revoking token.
	h.authenticationService.Logout(c, token)

	SendStatusResponse(ctx, http.StatusNoContent)
}

func (*authenticationHandler) abortWithStatusJSON(ctx *gin.Context, errorCode string) {
	ctx.AbortWithStatusJSON(http.StatusUnauthorized, libsecurity.HandlerErrorResponse{
		Error:     servercode.ServerCodeUnauthorized,
		Message:   errorCode,
		Path:      ctx.FullPath(),
		Status:    http.StatusUnauthorized,
		Success:   false,
		Timestamp: date.GetCurrentDate().Unix(),
	})
}

// Container of AuthenticationHandler functions.
type authenticationHandler struct {
	accountGroupService   service.AccountGroupService
	authenticationService service.AuthenticationService
	userService           service.UserService
}
